package docbookarchivemanager.utils;

import docbookarchivemanager.StartupManager;
import docbookarchivemanager.converter.Converter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.CopyOption;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;

/**
 *
 * @author Jakub Hrachovec
 */
public class FileManager {

    public static final String[] CONTENT_TYPES = {"text/xml", "application/xslt+xml", "text/xsl"};

    /**
     *
     * @param value
     * @param f
     * @return Returns file from given node (from absolute, relative path or the
     * Internet)
     * @throws java.io.IOException
     */
    @Deprecated
    public static File getFileFromNodeValue(String value, File f) throws IOException {
        String url = StringHelper.decodeURL(StringHelper.getPath(value));
        //First check if the current value is a valid url.
        if (StringHelper.isValidURL(url)) {
            //check if same url hasn't been already downloaded from.
            if (Converter.containsFileIdent(url)) {
                return new File(Converter.getFileIdentAndLocation(url));
            }
            //check if the file exists on given url.
            if (fileExistsAtUrl(url)) {
                //if it does download it and return it.
                File file = downloadFile(url, StartupManager.getTempFileDownloads().getAbsolutePath());
                if (file != null) {
                    Converter.addFileIdentAndLocation(url, file.getCanonicalPath());
                    return file;
                    //couldn't download file return null.  
                }
            }
            //if it isn't a valid url then the attribute points to a local file so return it.
        } else {
            return getAbsoluteFile(url, f);
        }
        return null;
    }

    /**
     * Returns local XML file. If path is a valid url or file doesn't locally
     * exists or file's content type isn't a valid one, returns null.
     *
     * @param path Path to the supposed file.
     * @param file File that contains a reference to the file.
     * @return local XML file
     */
    public static File getLocalXmlFile(String path, File file) {
        File temp = getLocalFile(path, file);
        if (temp != null && isFileAnXml(temp, path)) {
            return temp;
        }
        return null;
    }

    /**
     * Returns local file. If path is a valid url or file doesn't locally
     * exists, returns null.
     *
     * @param path Path to the supposed file.
     * @param file File that contains a reference to the file.
     * @return local file
     */
    public static File getLocalFile(String path, File file) {
        path = StringHelper.getPath(path);
        if (!StringHelper.isValidURL(path)) {
            File temp = getAbsoluteFile(path, file);
            if (fileExists(temp, path)) {
                return temp;
            }
        }
        return null;
    }

    /**
     * Copies given file to given location.
     *
     * @param sourceFile
     * @param copyTo
     * @param intoDar
     */
    public static void copyFile(File sourceFile, String copyTo, boolean intoDar) {
        String name = sourceFile.getName();
        try {
            Path FROM = Paths.get(sourceFile.getCanonicalPath());
            Path TO = Paths.get(copyTo);

            File check = new File(TO.toString());

            if (!check.exists()) {
                check.getParentFile().mkdirs();
                CopyOption[] options = new CopyOption[]{
                    StandardCopyOption.COPY_ATTRIBUTES
                };

                System.out.print("Copying file " + name);
                if (!intoDar) {
                    System.out.println(String.format(" to '%s'.", TO.toFile().getParentFile().getCanonicalPath()));
                } else {
                    System.out.println(" into the archive.");
                }

                Files.copy(FROM, TO, options);
            }

        } catch (FileNotFoundException | NoSuchFileException e) {
            System.out.println(String.format("ERROR: %s not found.", name));
        } catch (IOException e) {
            System.out.println("ERROR: IO Error Occurred for file: " + name + e.getMessage() + ".");
        }
    }

    /**
     * Copies given file to given location.
     *
     * @param sourceFile
     * @param copyTo
     */
    public static void copyFile(File sourceFile, String copyTo) {
        copyFile(sourceFile, copyTo, true);
    }

    /**
     * Downloads given file to given location.
     *
     * @param url
     * @param loc
     * @return downloaded file or null if there was an error
     */
    @Deprecated
    public static File downloadFile(String url, String loc) {
        ReadableByteChannel rbc = null;
        FileOutputStream fos = null;
        try {
            System.out.println("Downloading: " + url + ".");
            URL file = new URL(url);
            String name = url.substring(url.lastIndexOf("/") + 1, url.length());
            rbc = Channels.newChannel(file.openStream());
            File newFile = new File(loc + File.separator + name);
            if (newFile.exists()) {
                File temp = new File(loc + File.separator + UUID.randomUUID().toString());
                temp.mkdir();
                newFile = new File(loc + File.separator + temp.getName() + File.separator + name);
            }
            fos = new FileOutputStream(newFile.getAbsolutePath());
            fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);

            return newFile;
        } catch (IOException ex) {
            System.out.println(ex.getMessage());
            return null;
        } finally {
            try {
                fos.close();
                rbc.close();
            } catch (IOException ex) {
                System.out.println(ex.getMessage());
            }
        }

    }

    /**
     *
     * @param url
     * @return if a file exists at given location.
     */
    public static boolean fileExistsAtUrl(String url) {
        try {
            HttpURLConnection.setFollowRedirects(false);
            HttpURLConnection con
                    = (HttpURLConnection) new URL(url).openConnection();
            con.setRequestMethod("HEAD");
            if (con.getResponseCode() == HttpURLConnection.HTTP_OK) {
                return true;
            } else {
                System.out.println(String.format("ERROR: File doesn't exist at given url: %s", url));
                return false;
            }
        } catch (Exception ex) {
            return false;
        }

    }

    public static boolean isStylesheetAtUrl(String url) {
        if (StringHelper.isValidURL(url)) {
            if (FileManager.fileExistsAtUrl(url)) {
                if (StringHelper.arrayContains(CONTENT_TYPES, getContentTypeFromUrl(url))) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Checks if given file exists at given location.
     *
     * @param file file to check.
     * @param displayMessages if error messages should be displayed.
     * @param filePath expected path to the file.
     * @return if given file exists at given location
     */
    public static boolean fileExists(File file, boolean displayMessages, String filePath) {
        if (file != null) {
            if (file.exists() && file.isFile()) {
                return true;
            } else {
                if (displayMessages) {
                    try {
                        System.out.println(String.format("ERROR: File doesn't exist: %s", file.getCanonicalPath()));
                    } catch (IOException ex) {
                        System.out.println(String.format("ERROR: File doesn't exist: %s", file.getName()));
                    }
                }
            }
        } else {
            if (displayMessages && null != filePath) {
                System.out.println(String.format("ERROR: File doesn't exist: %s", filePath));
            }
        }
        return false;
    }

    /**
     * Checks if given file exists at given location.
     *
     * @param file file to check.
     * @param filePath expected path to the file.
     * @return if given file exists at given location
     */
    public static boolean fileExists(File file, String filePath) {
        return fileExists(file, true, filePath);
    }

    /**
     * Checks if given file has the same content types that were given.
     *
     * @param file file to check.
     * @param contentTypes Content types to check
     * @param displayMessages if error messages should be displayed.
     * @return if given file has the same content types that were given
     */
    public static boolean sameContentType(File file, String[] contentTypes, boolean displayMessages) {
        try {
            String contentType = Files.probeContentType(file.toPath());
            for (String s : contentTypes) {
                if (s.equals(contentType)) {
                    return true;
                }
            }
        } catch (IOException | SecurityException e) {
            System.out.println(e.getMessage());
        }
        if (displayMessages) {
            System.out.println(String.format("ERROR: %s is not an XML file.", file.getAbsolutePath()));
        }
        return false;
    }

    /**
     * Checks if given file has the same content types that was given.
     *
     * @param file file to check.
     * @param contentType Content type to check
     * @param displayMessages if error messages should be displayed.
     * @return if given file has the same content types that was given
     */
    public static boolean sameContentType(File file, String contentType, boolean displayMessages) {
        return sameContentType(file, new String[]{contentType}, displayMessages);
    }

    /**
     * Checks if given file has the same content types that was given.
     *
     * @param file file to check.
     * @param contentType Content type to check
     * @return if given file has the same content types that was given
     */
    public static boolean sameContentType(File file, String contentType) {
        return sameContentType(file, new String[]{contentType}, true);
    }

    /**
     *
     * @param urlname
     * @return content type
     */
    public static String getContentTypeFromUrl(String urlname) {
        try {
            URL url = new URL(urlname);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.setRequestMethod("HEAD");
            connection.connect();
            String contentType = connection.getContentType();
            return contentType;
        } catch (MalformedURLException ex) {
            System.out.println(String.format("ERROR: Malformed url %s", urlname));
        } catch (IOException ex) {
            System.out.println(String.format("ERROR: %s", ex.getMessage()));
        }
        return null;
    }

    /**
     * Checks if given file exists and its contentype is a valid one.
     *
     * @param file File to check.
     * @param filePath expected path to the file.
     * @return if given file exists and its contentype is a valid one.
     */
    public static boolean isFileAnXml(File file, String filePath) {
        return isFileAnXml(file, true, filePath);
    }

    /**
     * Checks if given file exists and its contentype is a valid one.
     *
     * @param file File to check.
     * @param displayMessages if error messages should be displayed.
     * @param filePath expected path to the file.
     * @return if given file exists and its contentype is a valid one.
     */
    public static boolean isFileAnXml(File file, boolean displayMessages, String filePath) {
        if (fileExists(file, displayMessages, filePath)) {
            if (sameContentType(file, CONTENT_TYPES, displayMessages)) {
                return true;
            }
        }
        return false;
    }

    /**
     * This will generate checksum (MD5) for given file. It is supposed to
     * prevent rewriting different files with same name and copying same files
     * with different names again.
     *
     * @param file
     * @return MD5 checksum
     */
    public static String generateChecksum(File file) {

        try (FileInputStream fis = new FileInputStream(file)) {
            MessageDigest md = MessageDigest.getInstance("MD5");

            byte[] dataBytes = new byte[1024];

            int nread = 0;
            while ((nread = fis.read(dataBytes)) != -1) {
                md.update(dataBytes, 0, nread);
            }
            byte[] mdbytes = md.digest();

            StringBuilder hexString = new StringBuilder();
            for (int i = 0; i < mdbytes.length; i++) {
                hexString.append(Integer.toHexString(0xFF & mdbytes[i]));
            }

            return hexString.toString().toUpperCase();

        } catch (NoSuchAlgorithmException | IOException ex) {
            System.out.println(ex.getMessage());
        }
        return null;

    }

    /**
     *
     * @param file
     * @return file name without extension
     */
    public static String getFileName(File file) {
        return getFileName(file.getName());
    }

    /**
     *
     * @param name
     * @return file name without extension
     */
    public static String getFileName(String name) {
        if (name.contains(".")) {
            if (name.contains(".tar.gz")) {
                return name.substring(0, name.lastIndexOf(".tar.gz"));
            }
            return name.substring(0, name.lastIndexOf("."));
        } else {
            return "";
        }
    }

    /**
     *
     * @param file
     * @return file extension without name
     */
    public static String getFileExtension(File file) {
        String name = file.getName();
        if (name.contains(".")) {
            if (name.contains(".tar.gz")) {
                return name.substring(name.lastIndexOf(".tar.gz"));
            }
            return name.substring(name.lastIndexOf("."));
        } else {
            return "";
        }
    }

    /**
     * Delets given file and all subfolders/subfiles.
     *
     * @param file
     */
    public static void deleteFile(File file) {
        try {
            if (!file.exists()) {
                return;
            }
            if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    deleteFile(f);
                }
            }
            Files.delete(file.toPath());
        } catch (IOException ex) {
            System.out.println(ex.getMessage());
        }
    }

    /**
     * If given file's path isn't absolute it will create a file with an
     * absolute path and returns it.
     *
     * @param curNodeValue
     * @param file
     * @param useGetParentIfNotDir if the method should use getParent file if
     * the given file isn't a directory.
     * @return File with absolute path.
     */
    public static File getAbsoluteFile(String curNodeValue, File file, boolean useGetParentIfNotDir) {
        String path = StringHelper.getPath(curNodeValue);
        File curNodeFile = new File(path);

        if (!curNodeFile.isAbsolute()) {
            if (useGetParentIfNotDir) {
                if (file.isDirectory()) {
                    curNodeFile = new File(file, path);
                } else {
                    curNodeFile = new File(file.getParentFile(), path);
                }
            } else {
                curNodeFile = new File(file, path);
            }
        }
        return curNodeFile;

    }

    /**
     * If given file's path isn't absolute it will create a file with an
     * absolute path and returns it.
     *
     * @param curNodeValue
     * @param file
     * @return File with absolute path.
     */
    public static File getAbsoluteFile(String curNodeValue, File file) {
        return getAbsoluteFile(curNodeValue, file, true);

    }

    /**
     * @param originalFile File to get a unique name for.
     * @param copyTo Location where the file is going to be copied.
     * @return A unique name for given file at given location.
     * @throws java.io.IOException
     */
    public static String getUniqueFileName(File originalFile, String copyTo) throws IOException {
        String fullFileName = originalFile.getName();
        File newFile = new File(copyTo + File.separator + fullFileName);

        int i = 0;
        while (Converter.containsFileLocation(newFile.getCanonicalPath())) {
            i++;
            newFile = new File(copyTo + File.separator + getFileName(originalFile) + "_" + i + getFileExtension(originalFile));
        }
        return newFile.getName();
    }

    /**
     * Checks if given file is in the same directory or subdirectory relative to
     * a file.
     *
     * @param fileToCheck
     * @param relative
     * @return if given file is in the same directory or subdirectory relative
     * to a file
     * @throws IOException
     */
    public static boolean isFileInSameDirOrSubdir(File fileToCheck, File relative) throws IOException {

        File parentOfFileToCheck = getFolder(fileToCheck);
        File parentOfRelative = getFolder(relative);

        String pathToParentOfFileToCheck = parentOfFileToCheck.getCanonicalPath();
        String pathToParentOfRelative = parentOfRelative.getCanonicalPath();

        return pathToParentOfFileToCheck.contains(pathToParentOfRelative);

    }

    private static File getFolder(File file) {
        File folder;
        if (file.isDirectory()) {
            folder = file;
        } else {
            folder = file.getParentFile();
        }
        return folder;
    }

    public static Stylesheet.IsFormatObject getFormatObject(String href) {
        if (href.endsWith("_fo.xsl")) {
            return Stylesheet.IsFormatObject.TRUE;
        }
        if (href.contains("http://docbook.sourceforge.net/release/xsl-ns/current/") || href.contains("http://docbook.sourceforge.net/release/xsl-ns/1.78.1/")) {
            if (href.contains("http://docbook.sourceforge.net/release/xsl-ns/current/fo/") || href.contains("http://docbook.sourceforge.net/release/xsl-ns/1.78.1/fo/")) {
                return Stylesheet.IsFormatObject.TRUE;
            } else {
                return Stylesheet.IsFormatObject.FALSE;
            }
        } else {
            return Stylesheet.IsFormatObject.UNKNOWN;
        }

    }

    public static Stylesheet.IsFormatObject getFormatObject(File file) {
        if (file.getName().endsWith("_fo.xsl")) {
            return Stylesheet.IsFormatObject.TRUE;
        } else {
            return Stylesheet.IsFormatObject.UNKNOWN;
        }
    }

    public static String getOutputFolder(File curNodeFile, String defaultOutputFolder) throws IOException {
        File mainFileParent = Converter.getUserEnteredDbFile().getParentFile();
        if (Converter.Mode.USER_MODE.equals(Converter.getMode())) {
            if (FileManager.isFileInSameDirOrSubdir(curNodeFile, mainFileParent)) {
                String temp = curNodeFile.getParentFile().getCanonicalPath().substring(mainFileParent.getCanonicalPath().length());
                if (temp.indexOf("\\") == 0) {
                    temp = temp.substring(1);
                }
                return temp;
            }
        }
        return defaultOutputFolder;
    }

}
